/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.support.scripting;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptException;

import fr.x9c.cadmium.kernel.Block;
import fr.x9c.cadmium.kernel.ByteCodeParameters;
import fr.x9c.cadmium.kernel.CadmiumException;
import fr.x9c.cadmium.kernel.Fail;
import fr.x9c.cadmium.kernel.Fatal;
import fr.x9c.cadmium.kernel.Interpreter;
import fr.x9c.cadmium.kernel.Value;
import fr.x9c.cadmium.support.Helper;
import fr.x9c.cadmium.util.CustomClassLoader;
import fr.x9c.cadmium.util.RandomAccessInputStream;
import fr.x9c.cadmium.util.RedirectedInputStream;
import fr.x9c.cadmium.util.RedirectedOutputStream;

import ocaml.compilers.cafesterolMain;


/**
 * This class implements a script engine based on Cadmium/Cafesterol. <br/>
 * Cadmium is used to run a modified Objective Caml toplevel for script
 * evaluation. Cafesterol is used to compile script that can be subsequently
 * run.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.1
 * @since 1.1
 */
public final class OCamlScriptEngine
    implements ScriptEngine, Compilable, Invocable {

    /** Identifier for next generated package (script compilation). */
    private static final AtomicInteger ID = new AtomicInteger();

    private static String[] primitiveProviders = {};

    /** Scripts context. */
    private ScriptContext context;

    /** Interpreter running toplevel. */
    private final Interpreter interpreter;

    /** Library paths. */
    private final List<String> libraryPaths;

    /** Loaded libraries, useful for script compilation. */
    private final List<String> libraries;

    /** Standard input. */
    private final RedirectedInputStream in;

    /** Standard output. */
    private final RedirectedOutputStream out;

    /** Standard error output. */
    private final RedirectedOutputStream err;

    /** Standard output. */
    private PrintStream printOut;
    private static OutputStream stdOut = System.out;

    /** Standard error output. */
    private PrintStream printErr;
    private static OutputStream stdErr = System.err;

    public static void setOutStream(OutputStream os){
        stdOut = os;
    }

    public static void setErrStream(OutputStream os){
        stdErr = os;
    }

    public static void setPrimitiveProviders(String[] providers){
        primitiveProviders = providers;
    }

    /**
     * Constructs a script engine by initializing a modified OCaml toplevel.
     * @throws RuntimeException if any error occurs during toplevel
     *                          initialization
     */
    public OCamlScriptEngine() throws RuntimeException {
        this.context = new OCamlContext();
        this.libraryPaths = new LinkedList<String>();
        this.libraryPaths.add("+cadmium");
        this.libraries = new LinkedList<String>();
        this.in = new RedirectedInputStream(System.in);
        this.out = new RedirectedOutputStream(stdOut);
        this.err = new RedirectedOutputStream(stdErr);
        this.printOut = new PrintStream(stdOut);
        this.printErr = new PrintStream(stdErr);
        try {
            final int SIZE = 1024;
            final byte[] buffer = new byte[SIZE];
            final File tmp = File.createTempFile("ocamlscript", ".toplevel");
            final InputStream is =
                OCamlScriptEngine.class.getResourceAsStream("script");
            final OutputStream os = new FileOutputStream(tmp);
            int read = is.read(buffer);
            while (read != -1) {
                os.write(buffer, 0, read);
                read = is.read(buffer);
            } // end while
            is.close();
            os.close();

            final RandomAccessInputStream bytecode =
                new RandomAccessInputStream(tmp);
            final ByteCodeParameters params =
                new ByteCodeParameters(new String[] {"-w", "a"},     /* args */
                                       false,                   /* backtrace */
                                       false,             /* exitStoppingJVM */
                                       this.in,                    /* std in */
                                       this.printOut,             /* std out */
                                       this.printErr,             /* std err */
                                       false,                         /* awt */
                                       false,                 /* bare canvas */
                                       false,                  /* javax.sound */
                                       false,                        /* jdbm */
                                       "Unix",                         /* os */
                                       false,              /* unix emulation */
                                       tmp.getAbsolutePath(),        /* file */
                                       true,                     /* embedded */
                                       null,                /* embedded base */
                                       false,                      /* dialog */
                                       false,                        /* help */
                                       false,                     /* version */
                                       false,                 /* disassemble */
                                       64 * 1024,         /* init stack size */
                                       64 * 1024,          /* max stack size */
                                       new String[0],           /* providers */
                                       true);         /* compile dispatchers */

            this.interpreter =
                    new Interpreter(params, new File("."), bytecode);
            bytecode.close();

            // adding primitives providers
            for (String prim : primitiveProviders) {
                this.interpreter.getContext().openLib(
                        Value.createFromBlock(Block.createString(prim)));
            }

            this.interpreter.execute();
        } catch (final IOException ioe) {
            throw new RuntimeException("Unable to create script engine", ioe);
        } catch (final Fatal.Exception fe) {
            throw new RuntimeException("Unable to create script engine", fe);
        } catch (final CadmiumException ce) {
            throw new RuntimeException("Unable to create script engine", ce);
        } catch (final Fail.Exception ex) {
            throw new RuntimeException("Unable to create script engine", ex);
        }

        // end try/catch
    } // end empty constructor

    /**
     * Adds a library path.
     * @param path library path to add
     * @throws NullPointerException if passed path is null
     * @throws ScriptException if an error occurs while trying to add directory
     */
    public void addLibraryPath(final String path)
        throws NullPointerException, ScriptException  {
        if (path != null) {
            if (!this.libraryPaths.contains(path)) {
                this.libraryPaths.add(path);
            } // end if
            try {
                this.interpreter.execute("javax.script.directory",
                                         Helper.createString(path));
            } catch (final CadmiumException ce) {
                throw new ScriptException(ce);
            } // end try/catch
        } else {
            throw new NullPointerException();
        } // end if/else
    } // end method 'addLibraryPath(String)'

    /**
     * Loads an OCaml library.
     * @param lib library name <b>with no extension</b>
     * @throws NullPointerException if passed library name
     * @throws ScriptException if an error occurs while trying to load library
     */
    public void loadLibrary(final String lib)
        throws NullPointerException, ScriptException {
        if (lib != null) {
            if (!this.libraries.contains(lib)) {
                this.libraries.add(lib);
            } // end if
            try {
                this.interpreter.execute("javax.script.load",
                                         Helper.createString(lib + ".cma"));
            } catch (final CadmiumException ce) {
                throw new ScriptException(ce);
            } // end try/catch
        } else {
            throw new NullPointerException();
        } // end if/else
    } // end method 'loadLibrary(String)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Bindings createBindings() {
        return new OCamlBindings();
    } // end method 'createBindings()'

    /**
     * {@inheritDoc}
     */
    @Override
    public Object eval(final Reader reader)
        throws NullPointerException, ScriptException {
        try {
            return evaluate(readerToString(reader), OCamlContext.instance(this.context));
        } catch (final IOException ioe) {
            throw new ScriptException(ioe);
        } // end try/catch
    } // end method 'eval(Reader)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Object eval(final Reader reader, final Bindings n)
        throws NullPointerException, ScriptException {
        try {
            return evaluate(readerToString(reader),
                            new OCamlContext(OCamlContext.instance(this.context),
                                             OCamlBindings.instance(n)));
        } catch (final IOException ioe) {
            throw new ScriptException(ioe);
        } // end try/catch
    } // end method 'eval(Reader, Bindings)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Object eval(final Reader reader, final ScriptContext context)
        throws NullPointerException, ScriptException {
        try {
            return evaluate(readerToString(reader), OCamlContext.instance(context));
        } catch (final IOException ioe) {
            throw new ScriptException(ioe);
        } // end try/catch
    } // end method 'eval(Reader, ScriptContext)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Object eval(final String script)
        throws NullPointerException, ScriptException {
        return evaluate(script, OCamlContext.instance(this.context));
    } // end method 'eval(String)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Object eval(final String script, final Bindings n)
        throws NullPointerException, ScriptException {
        return evaluate(script, new OCamlContext(OCamlContext.instance(this.context),
                                                 OCamlBindings.instance(n)));
    } // end method 'eval(String, Bindings)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Object eval(final String script, final ScriptContext context)
        throws NullPointerException, ScriptException {
        return evaluate(script, OCamlContext.instance(context));
    } // end method 'eval(String, ScriptContext)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Object get(final String key)
        throws NullPointerException, IllegalArgumentException {
        return getBindings(ScriptContext.ENGINE_SCOPE).get(key);
    } // end method 'get(String)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Bindings getBindings(final int scope)
        throws IllegalArgumentException {
        return this.context.getBindings(scope);
    } // end method 'getBindings(int)'

    /**
     * {@inheritDoc}
     */
    @Override
    public ScriptContext getContext() {
        return this.context;
    } // end method 'getContext()'

    /**
     * {@inheritDoc}
     */
    @Override
    public ScriptEngineFactory getFactory() {
        return new OCamlScriptEngineFactory();
    } // end method 'getFactory()'

    /**
     * {@inheritDoc}
     */
    @Override
    public void put(final String key, final Object value)
        throws NullPointerException, IllegalArgumentException {
        getBindings(ScriptContext.ENGINE_SCOPE).put(key, value);
    } // end method 'put(String, Object)'

    /**
     * {@inheritDoc}
     */
    @Override
    public void setBindings(final Bindings bindings, final int scope)
        throws NullPointerException, IllegalArgumentException {
        this.context.setBindings(bindings, scope);
    } // end method 'setBindings(Bindings, int)'

    /**
     * {@inheritDoc}
     */
    @Override
    public void setContext(final ScriptContext context)
        throws NullPointerException {
        this.context = context;
    } // end method 'setContext(ScriptContext)'

    /**
     * {@inheritDoc}
     */
    @Override
    public CompiledScript compile(final String script)
        throws NullPointerException, ScriptException {
        if (script == null) {
            throw new NullPointerException("null script");
        } // end if
        try {
            final int id = OCamlScriptEngine.ID.getAndIncrement();
            final String pack = "fr.x9c.cadmium.support.scripting.generated" + id;
            final File ml = File.createTempFile("ocamlscript", ".ml");
            final File jar = File.createTempFile("ocamlscript", ".jar");
            final Writer writer = new FileWriter(ml);
            writer.write(script);
            writer.close();
            final List<String> paramList = new LinkedList<String>();
            for (String s : this.libraryPaths) {
                paramList.add("-I");
                paramList.add(s);
            } // end for
            for (String s : this.libraries) {
                paramList.add(s + ".cmja");
            } // end for
            paramList.add("-w");
            paramList.add("a");
            paramList.add("-scripting");
            paramList.add("-cadmium-parameter");
            paramList.add("exitStoppingJVM=off");
            paramList.add("-java-package");
            paramList.add(pack);
            paramList.add("-o");
            paramList.add(jar.getAbsolutePath());
            paramList.add(ml.getAbsolutePath());
            final String[] params =
                paramList.toArray(new String[paramList.size()]);
            cafesterolMain.mainWithReturn(params);
            final URLClassLoader loader =
                new URLClassLoader(new URL[] {
                                       new URL("file://" + jar.getAbsolutePath())
                                   },
                                   CustomClassLoader.INSTANCE);
            final Class<?> cl = Class.forName(pack + ".cafesterolMain",
                                              true,
                                              loader);
            final Method main = cl.getMethod("mainScripting",
                                             String[].class,
                                             Map.class,
                                             InputStream.class,
                                             PrintStream.class,
                                             PrintStream.class);
            return new OCamlCompiledScript(this, main, this.context);
        } catch (final NoSuchMethodException nsme) {
            throw new ScriptException("Unable to compile script");
        } catch (final ClassNotFoundException cnfe) {
            throw new ScriptException("Unable to compile script");
        } catch (final IOException ioe) {
            throw new ScriptException(ioe);
        } // end try/catch
    } // end method 'compile(String)'

    /**
     * {@inheritDoc}
     */
    @Override
    public CompiledScript compile(final Reader script)
        throws NullPointerException, ScriptException {
        if (script == null) {
            throw new NullPointerException("null script");
        } // end if
        try {
            return compile(readerToString(script));
        } catch (final IOException ioe) {
            throw new ScriptException(ioe);
        } // end try/catch
    } // end method 'compile(Reader)'

    /**
     * Throws an <tt>UnsupportedOperationException</tt>.
     * @param thiz ignored
     * @param name ignored
     * @param args ignored
     * @return never
     * @throws ScriptException never
     * @throws NoSuchMethodException never
     * @throws NullPointerException never
     * @throws IllegalArgumentException never
     */
    public Object invokeMethod(final Object thiz,
                               final String name,
                               final Object... args)
        throws ScriptException, NoSuchMethodException, NullPointerException,
               IllegalArgumentException {
        throw new UnsupportedOperationException();
    } // end method 'invokeMethod(Object, String, Object...)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Object invokeFunction(final String name,
                                 final Object... args)
        throws ScriptException, NoSuchMethodException, NullPointerException {
        // flush current output streams
        try {
            this.out.flush();
            this.err.flush();
        } catch (final IOException ioe) {
            throw new ScriptException(ioe);
        } // end try/catch

        final OCamlContext ctxt = OCamlContext.instance(this.context);

        // set streams according to context
        this.in.redirect(ctxt.getStdIn());
        this.out.redirect(ctxt.getStdOut());
        this.err.redirect(ctxt.getStdErr());

        // call the OCaml eval function with script source and bindings
        try {
            final int len = args.length;
            final Value[] arguments = new Value[len];
            for (int i = 0; i < len; i++) {
                arguments[i] = OCamlBindings.convert(args[i]);
            } // end for
            return this.interpreter.execute(name, arguments);
        } catch (final CadmiumException ce) {
            throw new ScriptException(ce);
        } // end try/catch
    } // end method 'invokeFunction(String, Object...)'

    /**
     * Throws an <tt>UnsupportedOperationException</tt>.
     * @param clasz ignored
     * @param <T> ignored
     * @return never
     * @throws IllegalArgumentException never
     */
    public <T> T getInterface(final Class<T> clasz)
        throws IllegalArgumentException {
        throw new UnsupportedOperationException();
    } // end method 'getInterface(Class<T>)'

    /**
     * Throws an <tt>UnsupportedOperationException</tt>.
     * @param thiz ignored
     * @param clasz ignored
     * @param <T> ignored
     * @return never
     * @throws IllegalArgumentException never
     */
    public <T> T getInterface(final Object thiz, final Class<T> clasz)
        throws IllegalArgumentException {
        throw new UnsupportedOperationException();
    } // end method 'getInterface(Object, Class<T>)'

    /**
     * Actually evaluates a script.
     * @param script script to evaluate - should not be <tt>null</tt>
     * @param context evaluation context - should not be <tt>null</tt>
     * @return evaluation result, as a <tt>Value</tt> instance
     * @throws ScriptException if any error occurs during script evaluation
     */
    private Object evaluate(final String script,
                            final OCamlContext context)
        throws ScriptException {
        assert script != null : "null script";
        assert context != null : "null context";
        // flush current output streams
        try {
            this.out.flush();
            this.err.flush();
        } catch (final IOException ioe) {
            throw new ScriptException(ioe);
        } // end try/catch

        // set streams according to context
        this.in.redirect(context.getStdIn());
        this.out.redirect(context.getStdOut());
        this.err.redirect(context.getStdErr());

        // call the OCaml eval function with script source and bindings
        try {
            // the end-of-line character is appended solely for a better
            // display of error message, if any
            final String code = script + "\n";
            return this.interpreter.executeWithBindings("javax.script.eval",
                                                        context.getBindings(),
                                                        Helper.createString(code));
        } catch (final CadmiumException ce) {
            final Throwable cause = ce.getCause();
            if (cause instanceof Fail.Exception) {
                final Fail.Exception fe = (Fail.Exception) cause;
                final Value val =
                    fe.asValue(this.interpreter.getContext().getGlobalData());
                final Block blk = val.isBlock() ? val.asBlock() : null;
                final Value str = (blk != null) && (blk.sizeValues() >= 2) ? blk.get(1) : null;
                final Block msg = (str != null) && str.isBlock() ? str.asBlock() : null;
                if ((msg != null) && (msg.getTag() == Block.STRING_TAG)) {
                    throw new ScriptException(new Exception(msg.asString(),ce));
                } else {
                    throw new ScriptException(ce);
                } // end if/else
            } else if (cause instanceof Fatal.Exception) {
                throw new ScriptException(cause.getMessage());
            } else {
                throw new ScriptException(ce);
            } // end if/elsif/else
        } // end try/catch
    } // end method 'evaluate(String, OCamlContext)'

    /**
     * Reads the characters from a passed reader to build a string.
     * @param reader characters source - should not be <tt>null</tt>
     * @return the string constructed by characters read from passed reader
     * @throws IOException if an error occurs while reading characters
     */
    private static String readerToString(final Reader reader)
        throws IOException {
        assert reader != null : "null reader";
        final int SIZE = 1024;
        final StringWriter res = new StringWriter(SIZE);
        final char[] buffer = new char[SIZE];
        int read = reader.read(buffer);
        while (read != -1) {
            res.write(buffer, 0, read);
            read = reader.read(buffer);
        } // end while
        return res.toString();
    } // end method 'readerToString(Reader)'

} // end class 'OCamlScriptEngine'
